/**
 * 
 */
package com.online.pay.common.http;

import java.io.File;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.security.KeyStore;
import java.util.Properties;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;

import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpRequest;
import org.apache.http.HttpStatus;
import org.apache.http.ParseException;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.protocol.HttpContext;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;

/**
 * 初始化需要Client Authentication的httpsclient,如果tomcat上conf/server.xml文件中的Connector
 * 元素的clientAuth属性false（默认为false）,即使httpsclient加载了授权证书，效果与没有加载证书是一样的。
 * <p>
 * 默认将证书文件夹放在classpath的certificate目录下
 * </p>
 * <p>
 * 用法如下:
 * </p>
 * <p>
 * 一、
 * </p>
 * <p>
 * 如微信退款，在certificate文件下新建weixin文件夹，将证书文件和证书属性文件（文件名任意，但以.conf结尾）拷贝到该目录下，
 * </p>
 * <p>
 * 其中证书属性文件包含keyStoreName，keyStoreType，keyStorePassWord，keyPassWord属性
 * </p>
 * <p>
 * 二、
 * </p>
 * <p>
 * 定义bean，将新建的证书文件夹weixin配置为HttpsClient的属性keyStoreDirectory的值
 * </p>
 * <code> 
 *  &ltbean id="httpsClient" class="com.cmsz.xpay.framework.http.HttpsClient"&gt
		&ltproperty name="keyStoreDirectory" value="weixin" /&gt
    <br>
	&lt/bean&gt
	</code>
 * <p>
 * 三、
 * </p>
 * <p>
 * 注入所定义的bean
 * </p>
 * 
 * @author chenqiang
 *
 */
public class HttpsClient {

//	LogHandler logger = LogHandler.getLogger(HttpsClient.class);

	private static final String UTF_8 = "UTF-8";

	/** 连接池最大连接数 **/
	private int maxConnTotal;

	/** 每个路由最大连接数 **/
	private int maxConnPerRoute;

	/** 超时时间，秒 **/
	private int timeout;

	/** 重发次数 **/
	private int retryCount;

	/**
	 * 证书存放默认基础目录
	 */
	private final String DEFAULT_DIRECTORY = "certificate";

	/**
	 * keyStore目录
	 */
	private String keyStoreDirectory;

	/**
	 * 连接客户端
	 * 
	 */
	private CloseableHttpClient closeableHttpClient;

	/**
	 * 
	 */
	public HttpsClient() {
	}

	public void afterPropertiesSet() throws Exception {

		// 1、找到文件
		String path = this.getClass().getClassLoader().getResource(DEFAULT_DIRECTORY).getPath().toString();

		if (keyStoreDirectory == null || keyStoreDirectory == "") {
//			throw new UpayBusinessException("keyStoreDirectory为空", null);
		}

		File fileDir = new File(path + File.separator + keyStoreDirectory);
		File[] zipFiles = fileDir.listFiles();
		if (zipFiles == null || zipFiles.length == 0) {
//			throw new UpayBusinessException("keyStore不存在", null);
		}

		// 2、解析conf文件
		File[] confFiles = fileDir.listFiles(new FileFilter(".conf"));
		FileInputStream fileInputStream = new FileInputStream(confFiles[0]);
		Properties properties = new Properties();
		properties.load(fileInputStream);

		// 3、加载keystore
		String keyStoreType = properties.getProperty("keyStoreType");
		KeyStore keyStore = KeyStore.getInstance(keyStoreType);
		FileInputStream instream = new FileInputStream(
				new File(fileDir + File.separator + properties.getProperty("keyStoreName")));
		try {
			keyStore.load(instream, properties.getProperty("keyStorePassWord").toCharArray());
		} finally {
			instream.close();
		}

		// 4、初始化CloseableHttpClient

		String keyPassWord = keyStoreType.equals("PKCS12") ? properties.getProperty("keyStorePassWord")
				: properties.getProperty("keyPassWord");
		SSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(keyStore, keyPassWord.toCharArray()).build();

		// 超时时间,单位秒
		int httpReqTimeOut = timeout * 1000;
		// SSL套接字连接工厂,NoopHostnameVerifier为信任所有服务器
		SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext, NoopHostnameVerifier.INSTANCE);
		// 注册http套接字工厂和https套接字工厂
		Registry<ConnectionSocketFactory> r = RegistryBuilder.<ConnectionSocketFactory> create()
				.register("http", PlainConnectionSocketFactory.INSTANCE).register("https", sslsf).build();
		// 连接池管理器
		PoolingHttpClientConnectionManager pcm = new PoolingHttpClientConnectionManager(r);
		pcm.setMaxTotal(maxConnTotal);// 连接池最大连接数
		pcm.setDefaultMaxPerRoute(maxConnPerRoute);// 每个路由最大连接数
		/**
		 * 请求参数配置 connectionRequestTimeout: 从连接池中获取连接的超时时间，超过该时间未拿到可用连接，
		 * 会抛出org.apache.http.conn.ConnectionPoolTimeoutException: Timeout
		 * waiting for connection from pool connectTimeout:
		 * 连接上服务器(握手成功)的时间，超出该时间抛出connect timeout socketTimeout:
		 * 服务器返回数据(response)的时间，超过该时间抛出read timeout
		 */
		RequestConfig requestConfig = RequestConfig.custom().setConnectionRequestTimeout(httpReqTimeOut)
				.setConnectTimeout(httpReqTimeOut).setSocketTimeout(httpReqTimeOut).build();
		/**
		 * 构造closeableHttpClient对象
		 */
		closeableHttpClient = HttpClients.custom().setDefaultRequestConfig(requestConfig).setConnectionManager(pcm)
				.setRetryHandler(retryHandler).build();

	}

	/**
	 * 向指定地址发送报文 默认字符UTF-8
	 * 
	 * @param url
	 * @param message
	 * @return
	 * @throws ClientProtocolException
	 * @throws ParseException
	 * @throws IOException
	 */
	public String sendStringMessage(String url, String message)
			throws ClientProtocolException, ParseException, IOException {
		return sendStringMessage(url, message, UTF_8);
	}

	/**
	 * 向指定地址发送报文
	 * 
	 * @param url
	 * @param message
	 * @param charsetName
	 * @return
	 * @throws ClientProtocolException
	 * @throws ParseException
	 * @throws IOException
	 */
	public String sendStringMessage(String url, String message, String charsetName)
			throws ClientProtocolException, ParseException, IOException {

		// 字符串Entity
		StringEntity entity = null;
		entity = new StringEntity(message, Charset.forName(charsetName));
		// http post请求
		HttpPost httpost = new HttpPost(url);
		httpost.setEntity(entity);

		return httpRequest(httpost, charsetName);
	}

	/**
	 * http 请求
	 * 
	 * @param request
	 * @return 请求响应消息字符串
	 * @throws ClientProtocolExceptio
	 * @throws IOException
	 * @throws ParseException
	 */
	private String httpRequest(HttpUriRequest request, String charsetName)
			throws ClientProtocolException, ParseException, IOException {
		String responseText = null;
		CloseableHttpResponse response = null;
		HttpEntity entitys = null;
		try {
			response = closeableHttpClient.execute(request);
			if (HttpStatus.SC_OK == response.getStatusLine().getStatusCode()) {
				entitys = response.getEntity();
				if (entitys != null) {
					// 将返回实体转换为字符串
					responseText = EntityUtils.toString(entitys, Charset.forName(charsetName));
				}
			} else {
				// 放弃连接
				request.abort();
			}
		} catch (ClientProtocolException e) {
			throw e;
		} catch (ParseException e) {
			throw e;
		} catch (IOException e) {
			throw e;
		} finally {
			if (entitys != null) {
				try {
					// 释放资源可用触发连接放回连接池
					EntityUtils.consume(entitys);
				} catch (IOException e) {
					throw e;
				}
			}
		}
		return responseText;
	}

	/**
	 * 重发处理器
	 */
	private HttpRequestRetryHandler retryHandler = new HttpRequestRetryHandler() {

		@Override
		public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
			// 打印警告信息
			// logger.warn(String.format("HttpClient向%s发起请求失败，出现异常%s，第%d次进入重发处理器判断是否需要重发。",
			// context.getAttribute(HttpCoreContext.HTTP_TARGET_HOST),
			// exception,executionCount));
			if (executionCount >= retryCount) {
				// Do not retry if over max retry count
				return false;
			}
			if (exception instanceof ConnectTimeoutException) {
				// Connection refused
				return true;
			}
			if (exception instanceof UnknownHostException) {
				// Unknown host
				return true;
			}
			if (exception instanceof InterruptedIOException) {
				// Timeout
				return true;
			}
			if (exception instanceof SSLException) {
				// SSL handshake exception
				return false;
			}
			HttpClientContext clientContext = HttpClientContext.adapt(context);
			HttpRequest request = clientContext.getRequest();
			boolean idempotent = !(request instanceof HttpEntityEnclosingRequest);
			if (idempotent) {
				// 必须是幂等性的才能进行重发
				return true;
			}
			return false;
		}
	};

	/**
	 * 
	 * 使用后缀名过滤文件
	 *
	 */
	private class FileFilter implements FilenameFilter {

		private String suffix;

		public FileFilter(String suffix) {
			this.suffix = suffix;
		}

		@Override
		public boolean accept(File dir, String name) {
			if (name.toLowerCase().endsWith(suffix)) {
				return true;
			} else {
				return false;
			}
		}

	}

	public int getMaxConnTotal() {
		return maxConnTotal;
	}

	public void setMaxConnTotal(int maxConnTotal) {
		this.maxConnTotal = maxConnTotal;
	}

	public int getMaxConnPerRoute() {
		return maxConnPerRoute;
	}

	public void setMaxConnPerRoute(int maxConnPerRoute) {
		this.maxConnPerRoute = maxConnPerRoute;
	}

	public int getTimeout() {
		return timeout;
	}

	public void setTimeout(int timeout) {
		this.timeout = timeout;
	}

	public int getRetryCount() {
		return retryCount;
	}

	public void setRetryCount(int retryCount) {
		this.retryCount = retryCount;
	}

	public String getKeyStoreDirectory() {
		return keyStoreDirectory;
	}

	public void setKeyStoreDirectory(String keyStoreDirectory) {
		this.keyStoreDirectory = keyStoreDirectory;
	}

	public HttpRequestRetryHandler getRetryHandler() {
		return retryHandler;
	}

	public void setRetryHandler(HttpRequestRetryHandler retryHandler) {
		this.retryHandler = retryHandler;
	}
}
